/**
 *    TimedProfiles - let Android perform tasks based on conditions
 *    Copyright (C) 2011  Daniel Weinhold
 *
 *    This program is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation, either version 3 of the License, or
 *    (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * */
package de.drak.Profiles;

import java.util.Date;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.PowerManager;
import android.util.Log;

/**
 * This Class is does all the work in the background.
 * - Checks if conditions become true
 * - Fire settings if so
 * */
public class ConditionCecker extends Service {

	//private BroadcastReceiver mReceiver;
	/**Does the work*/private RuleChecker checker;
	private static PowerManager.WakeLock wl;
    /**Check all rules linked to a single plugin*/
	public static final String ACTION_CHECK_PLUGIN = "checkPlugin";
    /**Check all rules*/
	public static final String ACTION_TIMER = "periodicTimer";
    /**Check all rules with have reached their deadline*/
	public static final String ACTION_DEADLINE = "deadlineReached";

	@Override
	public void onCreate()
	{
		super.onCreate();
		
		checker = new RuleChecker(this.getApplicationContext());
		PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
		wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "drak.Profiles.ConditionCecker");
		
		Log.i("ConditionCecker", "Started");
	}
	@Override
	public void onDestroy()
	{
		super.onDestroy();
	}
	@Override
	public IBinder onBind(Intent arg0) {
		return null;
	}
	@Override
	public void onStart(Intent  intent, int startId)
	{
		//don't sleep while checking
		 wl.acquire();

		if(intent==null || intent.getAction()==null)
		{
			wl.release();
			return;
		}
		if(intent.getAction().equals(ACTION_TIMER))
		{
			checker.StartRuleCheck();
			return;
		}
		if(intent.getAction().equals(ACTION_DEADLINE))
		{
			checker.StartDeadlineRuleCheck();
			return;
		}
		if(intent.getAction().equals(ACTION_CHECK_PLUGIN))
		{
			checker.StartRuleCheckFor(intent.getStringExtra(TimedProfiles.INTENT_PACKAGE));
			return;
		}
		
		Log.i("ConditionCecker", String.format("onStart Got: %s",intent.getAction()));
		wl.release();
	}
	
	static void notify(final Context c, final int flags, final boolean vibrate, final CharSequence tickerText, final CharSequence contentTitle, final CharSequence contentText, Intent notificationIntent)
	{
		NotificationManager mNotificationManager = (NotificationManager) c.getSystemService(Context.NOTIFICATION_SERVICE);
		

		Notification notification = new Notification(R.drawable.icon, tickerText, System.currentTimeMillis());
		if(vibrate)
		{
			notification.defaults |= Notification.DEFAULT_VIBRATE;
		}
		notification .flags |= Notification.FLAG_AUTO_CANCEL | flags;
		
		PendingIntent contentIntent = PendingIntent.getActivity(c, 0, notificationIntent, 0);

		notification.setLatestEventInfo(c, contentTitle, contentText, contentIntent);
		
		mNotificationManager.notify(0, notification);		
	}

	/**
	 * This Class
	 * - Checks if conditions become true
	 * - Fire their settings if so
	 * */
	class RuleChecker
	{
		/**holds return values from plugins*/	private PlugInResult[] results;
		/**receiver for plugin results*/		private PluginReciever RuleRecievers;
		/**index for "results"*/				private int active_cond;

		/**cursor over rules*/					private Cursor RuleCursor;
		/**cursor over conditions*/				private Cursor SettingsCursor;

	    protected Database mDbHelper;
	    /**current state of DB*/				private int query_state;
	    
	    private Context context;
	    
	    /**to set retimer at the end*/			private boolean is_periodic_check; 
	    
	    RuleChecker(Context c)
	    {
	        mDbHelper = new Database(c);
			RuleRecievers = new PluginReciever();
			query_state = 0;
			context = c;
	    }
	    protected void finalize()
	    {
			if(query_state > 2)
			{
				SettingsCursor.close();
			}
			if(query_state > 1)
			{
				RuleCursor.close();
			}
			if(query_state > 0)
			{
				mDbHelper.close();
				wl.release();
			}
	    }
	    
	    private Handler handler = new Handler() {
	    	public void handleMessage(Message msg) {
	    	super.handleMessage(msg);
	    	returnFromRuleBroadcasts();
	    	}
	    };

	    class PlugInResult
	    {
	    	PlugInResult()
	    	{
	    		state = TimedProfiles.RESULT_CONDITION_UNKNOWN;
	    		next = -1;
	    	}
			public int state;
			public long next;
	    }
	    
	    class PluginReciever extends BroadcastReceiver
	    {
			@Override
			public void onReceive(Context context, Intent intent) {
				if(intent.getAction().equals(TimedProfiles.ACTION_CON_QUERY))
				{
					res.state = getResultCode();
					is_ready = true;
					handler.sendEmptyMessage(0);
					
					Bundle b = getResultExtras(false);
					if(b!=null && b.containsKey(TimedProfiles.INTENT_BUNDLE_NEXT_CHECK))
					{
						res.next = b.getLong(TimedProfiles.INTENT_BUNDLE_NEXT_CHECK);
					}
				}
			}
			PluginReciever()
			{
				super();
				reset();
			}
			public void reset()
			{
				res = new PlugInResult();
				is_ready = false;
			}
			public boolean is_ready;
			public PlugInResult res;
	    }
	    /**
		 * Trigger a periodic Rule Check
		 * 
		 * StartRuleCheck()
		 * do
		 * 		ckeckRule()
		 * 		do
		 * 			ckeckCondition()
		 * 			returnFromRuleBroadcasts()
		 * 		while SelNextCondition()
		 * while SelNextRule()
		 * */
		public void StartRuleCheck()
		{
			Log.i("ConditionCecker",  "StartRuleCheck");
			if(query_state != 0)
			{
				Log.e("ConditionCecker",  "RuleCheck already actuive -> exit");
				wl.release();
				return;
			}
			mDbHelper.open();
			query_state = 1;
			RuleCursor = mDbHelper.fetchActiveRules();
			if(RuleCursor ==null || RuleCursor.getCount()==0)
			{
				//there is nothing to do -> sleep
				TimedProfiles.deaktTimer(context);
				mDbHelper.close();
				wl.release();
				query_state = 0;
				return;
			}
			query_state = 2;
			is_periodic_check = true;
			ckeckRule();
		}
	    /**
		 * Trigger a deadline Rule Check
		 * 
		 * StartRuleCheck()
		 * do
		 * 		ckeckRule()
		 * 		do
		 * 			ckeckCondition()
		 * 			returnFromRuleBroadcasts()
		 * 		while SelNextCondition()
		 * while SelNextRule()
		 * */
		public void StartDeadlineRuleCheck()
		{
			Log.i("ConditionCecker",  "StartDeadlineRuleCheck");
			mDbHelper.open();
			query_state = 1;
			RuleCursor = mDbHelper.fetchDeadLineRules();
			if(RuleCursor ==null || RuleCursor.getCount()==0)
			{
				//there is nothing to do
				mDbHelper.close();
				wl.release();
				query_state = 0;
				return;
			}
			query_state = 2;
			is_periodic_check = false;
			ckeckRule();
		}
		
		public void StartRuleCheckFor(String editAkt)
		{
			Log.i("ConditionCecker",  "Checking "+editAkt);
			if(query_state != 0)
			{
				Log.e("ConditionCecker",  "RuleCheck already actuive -> exit");
				//TODO next check -1 auf 0 setzen
				wl.release();
				return;
			}
			mDbHelper.open();
			query_state = 1;
			RuleCursor = mDbHelper.fetchRules(editAkt);
			if(RuleCursor ==null || RuleCursor.getCount()==0)
			{
				//there is nothing to do
				mDbHelper.close();
				wl.release();
				query_state = 0;
				return;
			}
			/*
    		ConditionCecker.notify(context, 0, false,
    				"Requested check", context.getString(R.string.app_name), "Affected Rules: "+RuleCursor.getCount(),
					null);*/
    		
			query_state = 2;
			is_periodic_check = false;
			ckeckRule();
		}
		/**
		 * Check current Rule
		 * Prepares for Condition check
		 * */
		private void ckeckRule()
		{
			//next time to check
			final long time_c = RuleCursor.getLong(RuleCursor.getColumnIndex(Database.R_KEY_NEXT));
			final long now = System.currentTimeMillis();//SystemClock.elapsedRealtime();
			if(time_c > 1 && time_c > now)
			{
				//deadline not jet reached - rule has not jet changed
				Log.i("ConditionCecker",  "deadline in future");
				SelNextRule();
				return;
			}
			
			//get all conditions of rule
			SettingsCursor = mDbHelper.fetchRuleCondition(RuleCursor.getLong(RuleCursor.getColumnIndex(Database.R_KEY_ROWID)));
			if(SettingsCursor==null)
			{
				//no conditions, turn rule off
				mDbHelper.updateRule(RuleCursor.getLong(RuleCursor.getColumnIndex(Database.R_KEY_ROWID)), null, 0, -1, -1, -1);
				
				SelNextRule();
				return;
			}
			query_state = 3;
			
			//store anz of conditions to wait for
			results = new PlugInResult[SettingsCursor.getCount()];
			active_cond = 0;
			ckeckCondition();
		}
		/**
		 * Check current Condition
		 * Will send broadcast to the associated Plugin that will return in returnFromRuleBroadcasts()
		 * */
		private void ckeckCondition()
		{
			final Bundle bundle = mDbHelper.fetchPluginStateBundle(SettingsCursor.getLong(SettingsCursor.getColumnIndex(Database.RC_KEY_PLUGIN)));
			final ComponentName name = mDbHelper.fetchPluginStateFireComponent(SettingsCursor.getLong(SettingsCursor.getColumnIndex(Database.RC_KEY_PLUGIN)));
			
			RuleRecievers.reset();
			TimedProfiles.queryPlugInState(name, bundle, RuleRecievers, context);
			
			//returnFromRuleBroadcasts()
		}
		/**
		 * Select next Rule
		 * */
		private void SelNextRule()
		{
			if(RuleCursor.moveToNext() != false)
			{
				ckeckRule();
			}else{
				RuleCursor.close();
				query_state = 1;
				//nextDeadlineCheck

				int[] r = mDbHelper.fetchReadyRules();
				Log.i("ConditionCecker",  "active rules:"+r[0]+"/"+r[1]);
				if(r[0]>0)
				{
			        long d = mDbHelper.getNextDeadLine();
			        if(d!=0)
			        {
			        	TimedProfiles.aktTimerOnce(context, d);
			        }
				}
				if(r[1]>0)
				{
					if(!is_periodic_check)
					{
	    				TimedProfiles.aktTimer(context);
					}
	    		}else{
	    			TimedProfiles.deaktTimer(context);
				}
				//deadline ende
				mDbHelper.close();
				wl.release();
				query_state = 0;
			}
		}
		/**
		 * Select next Condition
		 * */
		private void SelNextCondition()
		{
			if(SettingsCursor.moveToNext() != false)
			{
				ckeckCondition();
			}else{
				SettingsCursor.close();
				query_state = 2;
	
				long nxt_check = -1;
				boolean set_on = true,
						was_on = (RuleCursor.getLong(RuleCursor.getColumnIndex(Database.R_KEY_ISON))==1),
						has_unknown = false;
				
				long last_true = -1, first_false = Long.MAX_VALUE;
				for(PlugInResult r : results)
				{
					if(r==null)
					{
						//happens when conditions are skiped, because one was false
						continue;
					}
					if(r.state==TimedProfiles.RESULT_CONDITION_SATISFIED)
					{
						set_on &= true;
						//if(r.next!=-1) //any rule could become false, so we cant ignore conditions without next field
						{
							//next is time the rule COULD become false
							first_false = Math.min(r.next, first_false);
						}
					}
					if(r.state==TimedProfiles.RESULT_CONDITION_UNSATISFIED)
					{
						set_on = false;
						if(r.next!=-1)
						{
							//next is time the rule COULD become true
							//all rules must become true, so we can ignore conditions without next field
							last_true = Math.max(r.next, last_true);
						}
					}
					if(r.state==TimedProfiles.RESULT_CONDITION_UNKNOWN)
					{
						has_unknown = true;
					}
				}

				if(!set_on)//dont care for unknown when one condition was false
				{
					has_unknown = false;
				}
				
				if(set_on != was_on && !has_unknown)
				{
	        		//rule status changed -> run tasks
					Log.i("ConditionCecker",  "running tasks");
					final Context c = context;
					final long id = RuleCursor.getLong(RuleCursor.getColumnIndex(Database.R_KEY_ROWID));
					
					if(set_on)
					{/*
		        		new Thread() {
		                    public void run() {
		                    	Database db = new Database(c);
		                    	db.open();*/
		                    	Cursor cu = mDbHelper.fetchRuleTasks(id);
		                    	if(cu != null)
		                    	{
		                			final int did = cu.getColumnIndex(Database.RT_KEY_DO),
		        					ii = cu.getColumnIndex(Database.RT_KEY_INK);
			                    	do{
			                			switch(Database.IntToTask(cu.getInt(did)))
			                	    	{
			                			case BIND_PROFILE:
			                	    	case START_PROFILE:
			            					Log.i("ConditionCecker",  "START_PROFILE"+cu.getLong(ii));
			                	    		TimedProfiles.ToggelProfile(mDbHelper, c, cu.getLong(ii), 1);
			                	    		break;
			                	    	case END_PROFILE:
			            					Log.i("ConditionCecker",  "END_PROFILE"+cu.getLong(ii));
			                	    		TimedProfiles.ToggelProfile(mDbHelper, c, cu.getLong(ii), 0);
			                	    		break;
			                	    	case DISCARD_ALL_PROFILES:
			            					Log.i("ConditionCecker",  "DISCARD_ALL_PROFILES");
			                			  	Cursor profilesCursor = mDbHelper.fetchAllProfiles();
	
			                				if(profilesCursor!=null)
			                				 {
			                				  	final int ia = profilesCursor.getColumnIndex(Database.P_KEY_ACTIVE),
			                						 	ir = profilesCursor.getColumnIndex(Database.P_KEY_ROWID);
			                				  do{
			                					  if(1==profilesCursor.getInt(ia))
			                					  {
			                						  TimedProfiles.ToggelProfile(mDbHelper, c, profilesCursor.getLong(ir),0);
			                					  }
			                				  }while(profilesCursor.moveToNext() != false);
			                				  profilesCursor.close();
			                				 }
			                	    		break;
	    								case DISABLE_SCANNING://TODO
			            					Date off = new Date();
			            					Log.i("ConditionCecker",  "DISABLE_SCANNING from "+off.toGMTString());
			            					off.setTime(off.getTime()+cu.getLong(ii));
			            					Log.i("ConditionCecker",  "DISABLE_SCANNING till "+off.toGMTString());
			            					TimedProfiles.deaktTimer(context);
			            					TimedProfiles.aktTimer(context, cu.getLong(ii));
	    									break;
	    									/*
			                	    	case LAUNCH_APP:
			                	    		//TODO
			                	    		break;*/
			                	    	case NOTIFY:
			                	    		
			                	    		//get name of rule
			                				Cursor ruleNameCrsr = mDbHelper.fetchRule(did);
			                				String ProfileName = "";
			                				if(ruleNameCrsr!=null)
			                				{
			                					ProfileName = ruleNameCrsr.getString(ruleNameCrsr.getColumnIndex(Database.R_KEY_NAME));
			                					ruleNameCrsr.close();
			                				}
			                				//send notification
			                	    		String msg = String.format(c.getString(R.string.rule_on_notify), ProfileName);
			                	    		ConditionCecker.notify(c, 0, false,
			                	    				msg, c.getString(R.string.app_name), msg,
			                						null);
			                	    		break;
			                	    	}
			                    	}while(cu.moveToNext() != false);
			                    	cu.close();
		                    	}
		                    	/*db.close();
		                    }
		        		}.start();*/
					}
					if(!set_on)
					{
		        		/*new Thread() {
		                    public void run() {
		                    	Database db = new Database(c);
		                    	db.open();*/
		                    	Cursor cu = mDbHelper.fetchRuleTasks(id);
		                    	if(cu != null)
		                    	{
		                			final int did = cu.getColumnIndex(Database.RT_KEY_DO),
		        					ii = cu.getColumnIndex(Database.RT_KEY_INK);
			                    	do{
			                			switch(Database.IntToTask(cu.getInt(did)))
			                	    	{
			                			case BIND_PROFILE:
			            					Log.i("ConditionCecker",  "END_PROFILE"+cu.getLong(ii));
			                	    		TimedProfiles.ToggelProfile(mDbHelper, c, cu.getLong(ii), 0);
			                	    		break;
			                	    	}
			                    	}while(cu.moveToNext() != false);
			                    	cu.close();
		                    	}
		                    	/*db.close();
		                    }
		        		}.start();*/
					}
				}
				
				//create next check stamp
				if(set_on && first_false != Long.MAX_VALUE)//rule is on - when do we get false
				{
					nxt_check = first_false;
				}else if(!set_on && last_true != -1)//rule is off - when do we get true
				{
					nxt_check = last_true;
				}
				
				if(nxt_check==-1 && RuleCursor.getLong(RuleCursor.getColumnIndex(Database.R_KEY_NEXT))!=0)
				{
					nxt_check = 0;//reset next check
				}
				
				//update DB: is it on or off & next check 
				mDbHelper.updateRule(RuleCursor.getLong(RuleCursor.getColumnIndex(Database.R_KEY_ROWID)), null, -1,
						has_unknown?-1:(set_on?1L:0L),
						nxt_check , -1);
				
				SelNextRule();
			}
		}
		/**
		 * Called when ordered broadcast receives the plugin receiver
		 * */
		public void returnFromRuleBroadcasts()
		{
			results[active_cond] = RuleRecievers.res;//TODO Err: Out of Bounds 1times at 1.8.2010
			++active_cond;
	
			if(RuleRecievers.res.state==TimedProfiles.RESULT_CONDITION_UNSATISFIED)
			{
				//one failed - so it cant bekomme true anymore
				SettingsCursor.moveToLast();
			}
			SelNextCondition();
		}
	}
}